package c.c.d.s.s.o.ts.as.configuration.support.client;

import c.c.d.s.s.o.ts.as.domain.client.dto.ClientDTO;
import c.c.d.s.s.o.ts.as.mapper.client.ClientMapper;
import cn.caplike.data.redis.service.spring.boot.starter.RedisKey;
import cn.caplike.data.redis.service.spring.boot.starter.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * 自定义的 {@link ClientDetailsService}
 *
 * @author LiKe
 * @version 1.0.0
 * @date 2020-06-15 12:41
 */
@Slf4j
@Service
public class CustomClientDetailsService implements ClientDetailsService {

    private ClientMapper clientMapper;

    private RedisService redisService;

    private PasswordEncoder passwordEncoder;

    /**
     * Description: 从数据库中获取已经注册过的客户端信息<br>
     * Details: 该方法会在整个认证过程中被多次调用, 所以应该缓存. 缓存过期时间在 access_token 有效期的基础上加一个时间 buffer
     *
     * @param clientId 客户端 ID
     * @see ClientDetailsService#loadClientByClientId(String)
     */
    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        log.debug("About to produce ClientDetails with client-id: {}", clientId);

        // ~ TODO [临时处理] 如果请求来自内部客户端 (资源服务器), 应该持久到专门的数据库表中
        // -------------------------------------------------------------------------------------------------------------

        if (StringUtils.equals("resource-server", clientId)) {
            return new CustomInnerClientDetails("resource-server", passwordEncoder.encode("resource-server-p"));
        }

        // ~ 如果请求来自第三方客户端
        // -------------------------------------------------------------------------------------------------------------

        final RedisKey cacheKey = RedisKey.builder().prefix("auth").suffix(clientId).build();

        // 先从缓存中获取 ClientDto
        ClientDTO clientDto = redisService.getValue(cacheKey, ClientDTO.class);
        // 如果缓存中没有, 从数据库查询并置入缓存
        if (Objects.isNull(clientDto)) {
            clientDto = clientMapper.getClient(clientId);

            if (Objects.isNull(clientDto)) {
                throw new ClientRegistrationException(String.format("客户端 %s 尚未注册!", clientId));
            }

            // Buffer: 10s
            redisService.setValue(cacheKey, clientDto, clientDto.getAccessTokenValidity() + 10);
        }

        return new CustomClientDetails(clientDto);
    }

    // ~ Inner Client
    // =================================================================================================================

    @Autowired
    public void setClientMapper(ClientMapper clientMapper) {
        this.clientMapper = clientMapper;
    }

    // ~ Autowired
    // =================================================================================================================

    @Autowired
    public void setRedisService(RedisService redisService) {
        this.redisService = redisService;
    }

    @Autowired
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    /**
     * Description: 内部客户端 (资源服务器)
     *
     * @author LiKe
     * @date 2020-07-21 20:18:10
     * @see org.springframework.security.oauth2.config.annotation.builders.ClientDetailsServiceBuilder.ClientBuilder
     */
    private static final class CustomInnerClientDetails extends BaseClientDetails {

        private static final Set<String> EMPTY_SET = new LinkedHashSet<>(0);

        private static final Map<String, Object> EMPTY_MAP = new LinkedHashMap<>(0);

        public CustomInnerClientDetails(String clientId, String secret) {
            this.setClientId(clientId);
            this.setClientSecret(secret);
            this.setAuthorizedGrantTypes(EMPTY_SET);
            this.setAccessTokenValiditySeconds(0);
            this.setRefreshTokenValiditySeconds(0);
            this.setRegisteredRedirectUri(EMPTY_SET);
            this.setScope(EMPTY_SET);
            this.setAuthorities(AuthorityUtils.createAuthorityList());
            this.setResourceIds(EMPTY_SET);
            this.setAdditionalInformation(EMPTY_MAP);
            this.setAutoApproveScopes(EMPTY_SET);
        }
    }
}
